# Copyright (C) 2007 Samuel Abels, http://debain.org
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2, as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
import re, tempfile, shutil, os.path, glob, commands
from Generator    import Generator
from FunctionCall import FunctionCall
from Term         import Term
from Regex        import Regex
from String       import String
from Variable     import Variable
from TokenList    import TokenList

re_modifier = re.compile(r'(?<!\\)\\([\w\.\-\^\[\]])', re.I)
string_re   = re.compile(r'(?<!\\)\$([^\d][\w_]+\b)',  re.I)

class CFunction(object):
    id_pool = 0

    def __init__(self, name = None, returns = 'void'):
        self.name    = name
        self.code    = []
        self.returns = returns
        self.indent  = 1
        if name is None:
            self.__class__.id_pool += 1
            self.name = 'anon_%02d' % self.__class__.id_pool

    def get_signature(self):
        return '%s %s(Parser* parser)' % (self.returns, self.name)

    def __str__(self):
        return self.get_signature() + ' {\n' \
             + '\n'.join(self.code) + '\n' \
             + '}\n'

    def add(self, string):
        # Decrease indent, if a scope is closed.
        if string[0] == '}' or string[-1] == '}':
            self.indent -= 1

        self.code.append('  ' * self.indent + string)

        # Increase indent, if a new scope was opened.
        if string[-1] == '{':
            self.indent += 1


class CMatcher(CFunction):
    def __init__(self, name = None, returns = 'inline short int'):
        CFunction.__init__(self, name, returns)
        self.indent += 1
        self.ctx     = self.name

    def __str__(self):
        return self.get_signature() + ' {\n' \
             + '  unsigned long n_matches = 0;\n' \
             + '  parser_debug(parser, 2, "Entering %s\\n");\n' % self.name \
             + '  xml_push(parser, NULL);\n' \
             + '  parser->stack_depth++;\n' \
             + '  do {\n' \
             + '    xml_pop(parser);\n' \
             + '    xml_push(parser, NULL);\n' \
             + '    if (lexer_is_done(parser->lexer)) {\n' \
             + '      xml_pop(parser);\n' \
             + '      break;\n' \
             + '    }\n' \
             + '\n'.join(self.code) + '\n' \
             + '    set_error(parser, "%s");\n' % self.ctx \
             + '    break;\n' \
             + '  } while (TRUE);\n' \
             + '  parser->stack_depth--;\n' \
             + '  xml_pop(parser);\n' \
             + '  parser_debug(parser, 3, "Leaving %s\\n");\n' % self.name \
             + '  return n_matches;\n' \
             + '}\n'


class CGenerator(Generator):
    """
    This class represents the generated parser (C language).
    """

    def __init__(self, **kwargs):
        """
        Constructor.
        """
        self.header        = []
        self.matchers      = []
        self.matcher_stack = []
        self.anon          = {}
        self.varname_map   = {}
        self.tmpdir        = None
        self.init          = CFunction('parser_init_gen')
        self.free          = CFunction('parser_free_gen')
        self.run           = CFunction('parser_run_gen')
        self.debug_level   = kwargs.get('debug_level', 0)


    def _dbg(self, level, string):
        if self.debug_level > level:
            print string


    def __str__(self):
        """
        Returns the resulting source code.
        """
        return '\n'.join(self.header
                       + ['/* Prototypes */']
                       + [m.get_signature() + ';' for m in self.matchers]
                       + ['/* Functions */']
                       + [m.__str__() for m in self.matchers]
                       + ['/* Start */']
                       + [self.init.__str__()]
                       + [self.free.__str__()]
                       + [self.run.__str__()])


    def _add_header(self, string):
        self.header.append(string)


    def _add_matcher(self, name = None):
        matcher = CMatcher(name)
        self.matchers.append(matcher)
        return matcher


    def _push_matcher(self, matcher):
        self.matcher_stack.append(matcher)
        matcher.ctx = '/'.join([m.name for m in self.matcher_stack])


    def _pop_matcher(self):
        self.matcher_stack = self.matcher_stack[:-1]


    def _replace_var_cb(self, match):
        varname = match.group(1)
        return '$%s' % self.varname_map[varname]


    def _replace_re_modifier_cb(self, match):
        modifier = match.group(1)
        if modifier in 'rnt':
            return match.group(0)
        elif modifier == 'd':
            return r'[:digit:]'
        elif modifier in '.^-[]':
            return r'\\' + modifier
        elif modifier == 'w':
            return r'[a-zA-Z0-9]'
        elif modifier == 's':
            return r'[ \t\r\n]'
        elif modifier == 'S':
            return r'[^ \t\r\n]'
        else:
            #return '\\' + match.group(0)
            raise Exception("Unknown modifier %s." % repr(modifier))


    def _get_anon_varname(self, varname):
        id = 1
        while self.anon.has_key(varname + str(id)):
            id += 1
        self.anon[varname + str(id)] = 1
        return '%s%02d' % (varname, id)


    ##################################################################
    # Built-in functions.
    ##################################################################
    def _func_text2xml_fail(self, token):
        ctx = self.matcher_stack[-1].ctx
        return (r'parser->stack_depth++;',
                r'set_error(parser, "fail() in %s");' % ctx,
                r'break;')


    def _func_text2xml_up(self, token):
        return (r'parser->stack_depth++;',
                r'break;')


    def _func_text2xml_out(self, token):
        status = 'TEXT2XML_STATUS_LEVEL_OUT'
        ctx    = self.matcher_stack[-1].ctx
        return (r'parser->stack_depth++;',
                r'parser->status = %s;' % status,
                r'parser_debug(parser, 3, "out() called in %s.\n");' % ctx,
                r'break;')


    def _func_text2xml_import(self, token, arg1):
        if isinstance(arg1.term, Variable):
            matcher = 'match_%s' % arg1.term.varname
        else:
            assert False # No such type
        return (r'result = %s(parser);' % matcher,
                r'parser_debug(parser, 3, "Left %s\n");' % matcher,
                #r'printf("RESULT WAS %li\n", result);',
                r'if (parser->status == TEXT2XML_STATUS_STOPPED',
                r' || parser->status == TEXT2XML_STATUS_FAILED) {',
                r'parser->stack_depth++;',
                r'break;',
                r'}',
                r'n_matches += result;',
                r'if (parser->status == TEXT2XML_STATUS_LEVEL_OUT) {',
                r'parser_debug(parser, 3, "%s matched, out.\n");' % matcher,
                r'parser->status = TEXT2XML_STATUS_RUNNING;',
                r'parser->stack_depth++;',
                r'break;',
                r'}',
                r'if (result > 0) {',
                r'parser_debug(parser, 3, "%s matched, repeat.\n");' % matcher,
                r'parser->stack_depth++;',
                r'continue;',
                r'}')


    def _func_xml_add(self, token, xml_path, data):
        str  = xml_path.term.string.replace(r'"', r'\"')
        str  = string_re.sub(self._replace_var_cb, str)
        data = data.term.string.replace(r'"', r'\"')
        data = string_re.sub(self._replace_var_cb, data)
        return (r'xml_cb_multi_processdata(parser, "%s", "%s");' % (str, data),
                r'if (parser->status == TEXT2XML_STATUS_STOPPED',
                r' || parser->status == TEXT2XML_STATUS_FAILED) {',
                r'parser->stack_depth++;',
                r'break;',
                r'}')


    def _func_xml_enqueue(self, token, var, xml_path):
        str     = xml_path.term.string.replace(r'"', r'\"')
        str     = string_re.sub(self._replace_var_cb, str)
        #term    = token.parent.get(var.term.varname)
        varname = r're_' + var.term.varname
        regex   = r're_get(parser, "%s")' % varname
        #print "VARNAME:", varname
        return (r'parser_enqueue(parser, %s, "%s");' % (regex, str),)


    def _func_xml_push(self, token, xml_path):
        str  = xml_path.term.string.replace(r'"', r'\"')
        str  = string_re.sub(self._replace_var_cb, str)
        return (r'xml_cb(parser, T_OPEN, "%s");' % str,
                r'xml_push(parser, "%s");' % str,
                r'if (parser->status == TEXT2XML_STATUS_STOPPED',
                r' || parser->status == TEXT2XML_STATUS_FAILED) {',
                r'parser->stack_depth++;',
                r'break;',
                r'}')


    ##################################################################
    # Token generators.
    ##################################################################
    def _generate_syntax(self, token):
        for child in token.children:
            self._generate(child)


    def _generate_global(self, token):
        for child in token.children:
            self._generate(child)


    def _generate_define(self, token):
        token.parent.define(**{token.varname: token.term})
        if isinstance(token.term.term, Variable):
            value = 're_%s' % token.term.term.varname
        else:
            regex = token.term.term.value()
            regex = re_modifier.sub(self._replace_re_modifier_cb, regex)
            value = 'regex_new("%s", 0)' % regex
        varname = 're_' + token.varname
        self.init.add(r'Regex* %s = re_add(parser, "%s", %s);' % (varname, varname, value))


    def _generate_grammar(self, token):
        matcher = self._add_matcher('match_' + token.varname)
        self._push_matcher(matcher)
        self._generate(token.body)
        self._pop_matcher()


    def _generate_grammarbody(self, token):
        for child in token.grammar_before:
            self._generate(child)
        for child in token.grammar:
            self._generate(child)
        for child in token.grammar_after:
            self._generate(child)


    def _generate_tokenlist(self, list):
        listname = self._get_anon_varname('tokens')

        # Create the list of tokens.
        lst = []
        for i, (matcher, alias) in enumerate(list.tokens):
            # The alias may contain named variables such as "$foo/mynode/$bar". 
            # Collect the names and map them to field numbers.
            if alias is None and isinstance(matcher, Variable):
                self.varname_map[matcher.varname] = i
            elif alias is None:
                pass
            elif '/' in alias:
                varname = alias.split('/')[-1]
                self.varname_map[varname] = i
            else:
                self.varname_map[alias] = i

            # Determine the type of match (regular expression or plain 
            # string).
            if isinstance(matcher, Variable):
                lst.append(('T_RE', 're_%s' % matcher.varname))
            elif isinstance(matcher, Regex):
                regex = matcher.value()
                regex = re_modifier.sub(self._replace_re_modifier_cb, regex)
                lst.append(('T_STR', '"%s"' % regex))
            else:
                assert False # No such type.

        # Define the list of tokens.
        args = ['%s, %s' % (t, v) for t, v in lst]
        args = ',\n                            '.join(args)
        self._add_header(r'static GList* %s = NULL;' % listname)
        self.init.add(r'%s = make_token_list(parser, %s, T_END);' % (listname, args))
        self.free.add(r'free_token_list(%s);' % listname)

        # Add the list of tokens to the matcher.
        self.matcher_stack[-1].add(r'if (parser_match(parser, %s)) {' % listname)
        self.matcher_stack[-1].add(r'n_matches++;')

        # Copy any resulting matches that have an XML path assigned 
        # into corresponding XML nodes.
        for i, (matcher, alias) in enumerate(list.tokens):
            if alias is None:
                continue
            elif alias.startswith('%'):
                alias  = alias[1:]
                single = True
            else:
                single = False

            # Evaluate the alias, replacing variable names by match 
            # numbers.
            alias = alias.replace(r'"', r'\"')
            alias = string_re.sub(self._replace_var_cb, alias)

            # Announce the resulting name.
            if single:
                self.matcher_stack[-1].add(r'xml_cb(parser, T_SINGLE, "%s");' % alias)
            else:
                match = r'parser->match_stack[parser->stack_depth]'
                value = r'(char*)vector_get(%s, %s)' % (match, i)
                self.matcher_stack[-1].add(r'xml_cb_multi(parser, "%s", %s);' % (alias, value))

            # The parser may have been stopped by a callback.
            self.matcher_stack[-1].add(r'if (parser->status == TEXT2XML_STATUS_STOPPED')
            self.matcher_stack[-1].add(r' || parser->status == TEXT2XML_STATUS_FAILED)')
            self.matcher_stack[-1].add(r'  break;')

        # Define the actions that are executed if the list produces a match.
        if list.body is not None:
            matcher = self._add_matcher()
            self._push_matcher(matcher)
            self._generate(list.body)
            self._pop_matcher()

            self.matcher_stack[-1].add(r'result = %s(parser);' % matcher.name)
            self.matcher_stack[-1].add(r'parser_debug(parser, 3, "Left %s\n");' % matcher.name)
            #self.matcher_stack[-1].add(r'printf("RESULT WAS %li\n", result);')
            self.matcher_stack[-1].add(r'n_matches += result;')
            self.matcher_stack[-1].add(r'if (parser->status == TEXT2XML_STATUS_FAILED')
            self.matcher_stack[-1].add(r' || parser->status == TEXT2XML_STATUS_STOPPED)')
            self.matcher_stack[-1].add(r'  break;')
            self.matcher_stack[-1].add(r'if (parser->status == TEXT2XML_STATUS_LEVEL_OUT) {')
            if not list.is_child_of(TokenList):
                self.matcher_stack[-1].add(r'parser->status = TEXT2XML_STATUS_RUNNING;')
            self.matcher_stack[-1].add(r'break;')
            self.matcher_stack[-1].add(r'}')

        self.matcher_stack[-1].add(r'continue;')
        self.matcher_stack[-1].add(r'}')


    def _generate_functioncall(self, token):
        funcname = '_func_' + token.funcname.replace('.', '_')
        func     = self.__getattribute__(funcname)
        result   = func(token, *token.arguments)
        if result is None:
            return
        self.matcher_stack[-1].add(r'parser->stack_depth--;')
        for line in result:
            self.matcher_stack[-1].add(line)
        self.matcher_stack[-1].add(r'parser->stack_depth++;')


    def _generate(self, token):
        token_name = token.__class__.__name__.lower()
        self.__getattribute__('_generate_' + token_name)(token)


    def _create_source_tree(self):
        # Copy the required files into a temporary directory.
        self.tmpdir = tempfile.mkdtemp()
        files       = os.path.join(os.path.dirname(__file__), 'CGenerator', '*')
        for file in glob.glob(files):
            if os.path.isdir(file):
                basename = os.path.basename(file)
                shutil.copytree(file, os.path.join(self.tmpdir, basename))
            else:
                shutil.copy(file, self.tmpdir)

        # Generate the plain C syntax parser.
        c_file = os.path.join(self.tmpdir, 'generated.c')
        self._dbg(1, "Writing the syntax to %s" % c_file)
        fp = open(c_file, 'w')
        fp.write(self.__str__() + '\n')
        fp.close()

        return c_file


    def _delete_source_tree(self):
        if not self.tmpdir:
            return
        shutil.rmtree(self.tmpdir)
        self.tmpdir = None


    def _check_requirements(self):
        for prog in ('gcc', 'pkg-config'):
            if not commands.getoutput('which ' + prog).endswith(prog):
                raise Exception(prog + ' is required but not installed')
        for lib in ('glib-2.0', 'libxml-2.0'):
            cmd = 'pkg-config --exists ' + lib
            if os.system(cmd) != 0:
                raise Exception('Required library ' + lib + '-dev not found')


    def _compile_libs(self):
        self._check_requirements()
        dir   = os.path.abspath(os.path.dirname(__file__))
        oldwd = os.getcwd()
        for lib in ('types', 'parser'):
            os.chdir(os.path.join(dir, 'CGenerator', lib))
            if os.system('make') != 0:
                raise Exception('Precompilation failed')
        os.chdir(oldwd)


    def _compile_shared_library(self, filename):
        self._check_requirements()
        c_file = self._create_source_tree()
        dir    = os.path.dirname(c_file)

        self._dbg(1, "Compiling %s as shared library to %s" % (c_file, filename))
        cmd = '''
        gcc -shared -w -Wl,-soname,$OUTFILE -o $OUTFILE \
            -fPIC -Wall -ggdb -std=c99 -pedantic-errors -D_GNU_SOURCE \
            -fgnu89-inline \
            `pkg-config --libs --cflags glib-2.0 libxml-2.0` \
            -I/usr/include/python2.5/ \
            $TMPDIR/parser.c \
            $TMPDIR/types/*.o $TMPDIR/parser/*.o'''
        cmd = cmd.replace('$TMPDIR',  dir)
        cmd = cmd.replace('$OUTFILE', filename)
        res = os.system(cmd)
        self._delete_source_tree()
        assert res == 0


    def _compile_executable(self, filename):
        self._check_requirements()
        c_file = self._create_source_tree()
        dir    = os.path.dirname(c_file)

        self._dbg(1, "Compiling %s as executable binary to %s" % (c_file, filename))
        cmd = '''
        gcc -Wall -ggdb -std=c99 -pedantic-errors -D_GNU_SOURCE \
            -fgnu89-inline \
            `pkg-config --libs --cflags glib-2.0 libxml-2.0` \
            $TMPDIR/parser.c $TMPDIR/main.c -o $OUTFILE \
            $TMPDIR/types/*.o $TMPDIR/parser/*.o'''
        cmd = cmd.replace('$TMPDIR',  dir)
        cmd = cmd.replace('$OUTFILE', filename)
        res = os.system(cmd)
        self._delete_source_tree()
        assert res == 0


    def _compile_python_module(self, outfile):
        self._check_requirements()
        dir    = os.path.dirname(__file__)
        infile = os.path.join(dir, 'CGenerator', 'pytextparser.c')
        cmd = '''
        gcc -shared -Wl,-soname,pytextparser.so -o $OUTFILE \
            -fPIC -Wall -ggdb -std=c99 -pedantic-errors -D_GNU_SOURCE \
            -fgnu89-inline \
            `pkg-config --libs --cflags glib-2.0 libxml-2.0` \
            -I/usr/include/python2.5/ \
            $INFILE'''
        cmd = cmd.replace('$INFILE',  infile)
        cmd = cmd.replace('$OUTFILE', outfile)
        res = os.system(cmd)
        self._delete_source_tree()
        assert res == 0


    def compile(self, parse_tree, **kwargs):
        if len(self.header) == 0:
            self._generate(parse_tree)
        if kwargs.has_key('shared_lib'):
            self._compile_shared_library(kwargs.get('shared_lib'))
        if kwargs.has_key('executable'):
            self._compile_executable(kwargs.get('executable'))
